// Copyright 2011 Douglas Lacher, All Rights Reserved.

package com.devioustoast.turretdroid;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.media.MediaPlayer;
import android.util.Log;

/**
 * This class represents a floor turret from Portal and Portal 2.
 * 
 * The sound files directory is expected to contain a sub-directory for
 * each action of the turret.  Those sub-directories should contain sound
 * files corresponding to those actions.  A map of the sub-directory
 * (each action) to its files is stored.  Most actions will pick a random
 * sound for its action type (again, the sub-directory).
 * 
 * @author Douglas Lacher <doug.lacher@gmail.com>
 */
public class Turret {
  private static final String LOG_TAG = Turret.class.getSimpleName();
  private static final String[] SOUND_FILE_SUFFIXES = {".mp3", ".wav"};
  private static final ComparableSensorEvent DEFAULT_SENSOR_EVENT =
      new ComparableSensorEvent(
          0, 0, new float[]{0.0f, 0.0f, 0.0f}, new float[]{0.0f, 0.0f, 0.0f});
  public static final ComparableSensorEvent MOVEMENT_EVENT =
      new ComparableSensorEvent(0, 1001, new float[]{}, new float[]{});
  private static MediaPlayer mMediaPlayer;
  private ComparableSensorEvent mLastEvent = DEFAULT_SENSOR_EVENT;
  private Map<ComparableSensorEvent, File> mEventDirectoryMap =
      new HashMap<ComparableSensorEvent, File>();
  private String mStartupSoundsPath = "";
  
  public Turret(MediaPlayer mediaPlayer) {
    Turret.mMediaPlayer = mediaPlayer;
  }
  
  /**
   * Binds a sensor event to a directory of sound files.  When the event
   * occurs, a random sound from the provided directory is played.
   * 
   * @param soundDirectory the directory containing sound files.
   * @param event the event on which to trigger.
   */
  public void bindSoundsToEvent(File directory, ComparableSensorEvent event) {
    if (directory.isDirectory()) {
      mEventDirectoryMap.put(event, directory);
    } else {
      Log.w(
          LOG_TAG,
          String.format("'%s' is not a valid directory.", directory.getPath()));
    }
  }
  
  /**
   * Plays a random sound for the provided event if there are sound files
   * registered for the provided event.
   * 
   * @param event a SensorEvent that has been registered with a directory of
   *        sound files.
   * @see #bindSoundsToEvent(File, SensorEvent) for how to register an event.
   */
  public void playRandomSoundForEvent(SensorEvent event) {
    ComparableSensorEvent storedEvent = findEvent(event);
    if (storedEvent != null && !mLastEvent.equals(storedEvent)) {
      mLastEvent = storedEvent;
      playRandomSound(mEventDirectoryMap.get(storedEvent));
    }
  }
  
  /**
   * Plays a random sound suitable for when movement is detected by the camera.
   * This method intentionally ignores the last event tracker.
   */
  public void playRandomSoundForMovement() {
    playRandomSound(mEventDirectoryMap.get(MOVEMENT_EVENT));
  }
  
  public void resetLastEvent() {
    mLastEvent = DEFAULT_SENSOR_EVENT;
  }
  
  /**
   * Checks the provided event against this Turret's registered events.  If
   * the provided event is similar to one of the events
   * 
   * @param event
   * @return a directory containing sound files or null if no event was found.
   */
  public ComparableSensorEvent findEvent(SensorEvent event) {
    if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
      for (ComparableSensorEvent other : mEventDirectoryMap.keySet()) {
        if (other.isWithInRange(event)) {
//          Log.d(LOG_TAG, String.format(
//              "Event values {%f %f %f} are within range of " +
//              " {%f %f %f}-{%f %f %f}.",
//              event.values[0], event.values[1], event.values[2],
//              other.minValues[0], other.minValues[1], other.minValues[2],
//              other.maxValues[0], other.maxValues[1], other.maxValues[2]));
          return other;
        }
      }
    }
    return null;
  }
  
  /**
   * Stops the current sound playback in progress, if any.
   */
  public void stopPlaying() {
    Log.d(LOG_TAG, "Stopping playback.");
    mMediaPlayer.stop();
  }
  
  /**
   * Play the provided audio file if the file is available and not currently
   * busy.
   * 
   * @param file An audio File to play.
   * @throws SecurityException 
   * @throws IOException 
   * @throws IllegalStateException 
   * @throws IllegalArgumentException 
   */
  public void playSound(File file) {
    if (isFileAvailable(file) && !mMediaPlayer.isPlaying()) {
      try {
        Log.d(LOG_TAG, "Playing " + file.getPath() + ".");
        mMediaPlayer.reset();
        mMediaPlayer.setDataSource(file.getPath());
        mMediaPlayer.prepare();
        mMediaPlayer.start();
      } catch (IllegalArgumentException e) {
        Log.e(LOG_TAG, String.format(
            "Invalid file name to play, '%s'.", file.getPath()), e);
      } catch (SecurityException e) {
        Log.e(LOG_TAG, String.format(
            "Security exception thrown while playing sound file, '%s'.",
            file.getPath()), e);
      } catch (IllegalStateException e) {
        Log.e(LOG_TAG, String.format(
            "Illegal state exception thrown while playing sound file, '%s'.",
            file.getPath()), e);
      } catch (IOException e) {
        Log.e(LOG_TAG, String.format(
            "IO exception thrown while playing sound file, '%s'.",
            file.getPath()), e);
      }
    }
  }
  
  /**
   * Play a random sound from the provided directory.
   * 
   * @param directory the directory from which to play a random sound.
   * @throws IOException 
   * @throws IllegalStateException 
   * @throws IllegalArgumentException 
   */
  public void playRandomSound(File directory) {
    File[] files = directory.listFiles(new SoundFileFilter());
    Integer index = new Random(new Date().getTime()).nextInt(files.length);
    playSound(files[index]);
  }
  
  private boolean isFileAvailable(File file) {
    return (file.exists() && file.canRead() && file.isFile());
  }
  
  private class SoundFileFilter implements FileFilter {
    public boolean accept(File file) {
      for (String suffix : SOUND_FILE_SUFFIXES) {
        if (file.getName().endsWith(suffix)) {
          return true;
        }
      }
      return false;
    }
  }
  
  public void setStartupSoundsPath(String path) {
    this.mStartupSoundsPath = path;
  }
  
  public String getStartupSoundsPath() {
    return this.mStartupSoundsPath;
  }
  
  /**
   * A fake SensorEvent object that mimics the real SensorEvent enough to
   * make a comparison.  Values lower and upper bounds are inclusive.
   */
  public static class ComparableSensorEvent {

    int accuracy;
    int sensorType;
    float[] minValues;
    float[] maxValues;
    
    /**
     * @param accuracy @see {@link SensorEvent#accuracy}
     * @param sensorType @see {@link SensorEvent#sensor}
     * @param minValues @see {@link SensorEvent#values} values lower bound.
     * @param maxValues @see {@link SensorEvent#values} values upper bound.
     */
    ComparableSensorEvent(
        int accuracy, int sensorType, float[] minValues, float[] maxValues) {
      this.accuracy = accuracy;
      this.sensorType = sensorType;
      this.minValues = minValues;
      this.maxValues = maxValues;
    }
    
    /**
     * Compares this sensor event to a real sensor event and returns "true"
     * if the real event is within the min and max values of this event.
     * 
     * @see {@link http://developer.android.com/reference/android/hardware/SensorEvent.html}
     * for how to interpret the values array of each event type.
     * 
     * @param other
     * @return
     */
    boolean isWithInRange(SensorEvent other) {
      boolean inRange = true;
      if (sensorType == other.sensor.getType() &&
          minValues.length == other.values.length) {
//        Log.d(LOG_TAG, String.format(
//            "Sensor values of event: %s x %s x %s",
//            other.values[0], other.values[1], other.values[2]));
//        Log.d(LOG_TAG, String.format(
//            "Min values: %s x %s x %s",
//            minValues[0], minValues[1], minValues[2]));
//        Log.d(LOG_TAG, String.format(
//            "Max values: %s x %s x %s",
//            maxValues[0], maxValues[1], maxValues[2]));
        for (int i = 0; i < minValues.length; i++) {
          if (other.values[i] < minValues[i] ||
              maxValues[i] < other.values[i]) {
//            Log.i(LOG_TAG, String.format(
//                "%s is either too small (%s) or too big(%s).",
//                other.values[i], minValues[i], maxValues[i]));
            inRange = false;
          }
        }
      } else {
        inRange = false;
      }
      return inRange;
    }
    
    boolean equals(ComparableSensorEvent other) {
      if (accuracy == other.accuracy &&
          sensorType == other.sensorType &&
          minValues == other.minValues &&
          maxValues == other.maxValues) {
        return true;
      }
      return false;
    }
  }
}
